在 Day 1.
的規劃,今天主題是零星補充這個實踐系列的內容,那就繼續延續上一篇,來探討所謂測試覆蓋率高的程式碼,可能會長怎麼樣呢?
原始程式碼:
//根據圖幅編號拿到該圖幅編號第一筆資料的SHAPE
public string GetFrameList_SHAPE(string frame_id)
{
var SHAPE = "";
try
{
var cnStr = _config.GetConnectionString("apiAuthConnection");
// 可拆 1. 根據 frame_id 找到 shape 字串
string sqlStr = @"SELECT SHAPE.ToString() AS Shape
FROM FRAME WHERE frame_id = @frame_id;";
using (SqlConnection conn = new SqlConnection(cnStr))
{
SHAPE = conn.Query<string>(sqlStr, new { frame_id }).FirstOrDefault();
// 可拆 2. shape 資料使用 WKTReader 轉成 polygon 物件
var reader = new WKTReader();
var geometry = reader.Read(SHAPE) as Polygon;
if (geometry != null)
{
// 可拆 3. 從 polygon 取得資料並組 JSON 陣列
var coordinates = geometry.ExteriorRing.Coordinates;
var jsonArray = new List<List<List<double>>>();
foreach (var coordinate in coordinates)
{
jsonArray.Add(new List<List<double>> { new List<double> { coordinate.X, coordinate.Y } });
}
var jsonString = JsonConvert.SerializeObject(jsonArray);
Console.WriteLine(jsonString);
}
}
}
catch (Exception ex)
{
throw;
}
return SHAPE;
}
重構後的程式碼:
將原本單一方法,拆分成好幾個小方法。
public class FrameShapeService
{
private readonly IDbConnection _dbConnection;
private readonly IWKTReader _wktReader;
private readonly ILogger<FrameShapeService> _logger;
public FrameShapeService(IDbConnection dbConnection, IWKTReader wktReader, ILogger<FrameShapeService> logger)
{
_dbConnection = dbConnection;
_wktReader = wktReader;
_logger = logger;
}
public async Task<string> GetFrameShapeAsync(string frameId)
{
try
{
var shape = await QueryFrameShapeAsync(frameId);
var geometry = ParseWKT(shape);
return SerializeGeometry(geometry);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting frame shape for frame ID {FrameId}", frameId);
throw;
}
}
private async Task<string> QueryFrameShapeAsync(string frameId)
{
const string sql = "SELECT SHAPE.ToString() AS Shape FROM FRAME WHERE frame_id = @frameId";
return await _dbConnection.QueryFirstOrDefaultAsync<string>(sql, new { frameId });
}
private Polygon ParseWKT(string wkt)
{
return _wktReader.Read(wkt) as Polygon;
}
private string SerializeGeometry(Polygon geometry)
{
if (geometry == null) return null;
var coordinates = geometry.ExteriorRing.Coordinates;
var jsonArray = coordinates.Select(c => new List<List<double>> { new List<double> { c.X, c.Y } }).ToList();
return JsonConvert.SerializeObject(jsonArray);
}
}
測試修改後的程式碼:
public class FrameShapeServiceTests
/*
測試輸入圖框編號,是否會正確回傳shap資料
期望成果 1. return 一個簡單的 POLYGON 字串。
2. WKTReader 會將輸入的 shape 字串解析為一個 Polygon 物件。
*/
{
[Fact]
public async Task GetFrameShapeAsync_ShouldReturnSerializedShape()
{
// Arrange
var mockDbConnection = new Mock<IDbConnection>();
mockDbConnection.Setup(db => db.QueryFirstOrDefaultAsync<string>(It.IsAny<string>(), It.IsAny<object>(), null, null, null))
.ReturnsAsync("POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))");
var mockWktReader = new Mock<IWKTReader>();
mockWktReader.Setup(r => r.Read(It.IsAny<string>()))
.Returns(new Polygon(new LinearRing(new Coordinate[] {
new Coordinate(0, 0),
new Coordinate(0, 1),
new Coordinate(1, 1),
new Coordinate(1, 0),
new Coordinate(0, 0)
})));
var mockLogger = new Mock<ILogger<FrameShapeService>>();
var service = new FrameShapeService(mockDbConnection.Object, mockWktReader.Object, mockLogger.Object);
// Act:執行 GetFrameShapeAsync 方法
// 傳入一個虛擬的 frame_id,測試方法是否能正確執行並 return 序列化的 JSON 結果。
var result = await service.GetFrameShapeAsync("testFrameId");
// Assert:這部分檢查 return 的結果,確認它符合期望的 JSON 格式且不為 null。
Assert.NotNull(result);
var expectedJson = "[[[[0.0,0.0]]],[[[0.0,1.0]]],[[[1.0,1.0]]],[[[1.0,0.0]]],[[[0.0,0.0]]]]";
Assert.Equal(expectedJson, result);
}
}
今天分享了一個可測試性的程式碼案例,而單元測試通常在專門的測試專案中撰寫和執行,而不是直接在 Controller 中執行方法。這樣不僅可提高程式碼的可測試性,還能確保測試的獨立性和穩定性。此外,這些測試也可以在持續集成(CI
)工具中自動執行,進一步提升開發流程的效率和可靠性。